﻿using System;
using System.Linq;
using System.Text.RegularExpressions;
using Db4objects.Db4o;
using Db4objects.Db4o.Linq;
using SharedObjects;
using TheGame.Server;
using TheGameLib;

namespace TheGame.Webservices
{
	public abstract class EngineInterfaceBase
	{
		private IObjectContainer _db;
		protected IObjectContainer Db
		{
			get
			{
				return _db;
			}
			set { _db = value; }
		}

		protected ITheGameServiceCallback Callback;
		protected static Regex EmailAddressParser;
		protected static Regex SystemAddressParser;

		protected EngineInterfaceBase()
		{
			_db = World.Instance.ObtainDatabaseClient();
			if(EmailAddressParser == null)
				EmailAddressParser = new Regex(@"\A(?<name>\w+)@(?<host>\w+)\.(?<corp>\w+)\.(?<tld>\w+)\Z", RegexOptions.Compiled);
			if(SystemAddressParser == null)
				SystemAddressParser = new Regex(@"\A(?<host>\w+)\.(?<corp>\w+)\.(?<tld>\w+)\Z", RegexOptions.Compiled);
		}

		~EngineInterfaceBase()
		{
			if(_db != null)
				_db.Close();
			_db = null;
			Console.WriteLine("Client service channel dtor().");
		}

		#region Agent session validation
		protected bool CheckIsValidAgent(Guid sessionId)
		{
			return CheckIsValidAgent(sessionId, false);
		}

		protected bool CheckIsValidAgent(Guid sessionId, bool needsAdmin)
		{
			Agent thisAgent;
			return CheckIsValidAgent(sessionId, out thisAgent, needsAdmin);
		}

		protected bool CheckIsValidAgent(Guid sessionId, out Agent agent)
		{
			return CheckIsValidAgent(sessionId, out agent, false);
		}

		public bool CheckIsValidAgent(Guid sessionId, out Agent agent, bool adminRequired)
		{
			agent = null;
			AgentSessionObject aso = World.Instance.GetAgentSession(sessionId);
			if (aso == null)
				return false;
			
			lock (aso)
			{
				ulong agentId = aso.AgentInDb.Id;
				if(!Db.Ext().IsActive(aso.AgentInDb))
					Db.Ext().Activate(aso.AgentInDb);
				Agent thisAgent;

				if (!World.Instance.TryGetAgentFromCache(aso.AgentInDb.Id, out thisAgent))
				{
					if (thisAgent == null) //agent might be newly created and not be in the cache at this point
					{
						thisAgent = (from Agent a in Db where a.Id == agentId select a).FirstOrDefault();
						if (thisAgent == null)
							return false;
						World.Instance.UpdateAgentCache();
					}
				}

// ReSharper disable AccessToModifiedClosure
				var checkagent = (from Agent a in Db where a.Id == thisAgent.Id select a).FirstOrDefault();
// ReSharper restore AccessToModifiedClosure
				if (checkagent == null)
					return false;
				
				thisAgent = checkagent;


				//(optional) is the Agent admin)
				if (adminRequired)
				{
					var acct = (from Account a in Db where a.Agent.Id == agentId && a.IsAdmin select a).Count();
					if (acct != 1)
						return false;
				}

				agent = thisAgent;

				//update LastActive timestamp in online-list
				World.Instance.UpdateAgentSessionLifetime(sessionId);
			}
			return true;
		}

		public bool ValidateAgentSessionAndConnection(Guid sessionId, ulong systemId, out Agent me, out AgentSessionObject aso, out AgentConnection conn, bool needsAdmin = false)
		{
			aso = null;
			conn = null;

			if (!CheckIsValidAgent(sessionId, out me, needsAdmin))
				return false;

			aso = World.Instance.GetAgentSession(sessionId);
			if (aso == null)
				return false;

			if (systemId == 0)
				return true;

			conn = aso.GetAgentConnectionByTargetSystem(systemId);
			if (conn == null)
				return false;

			return true;
		}
		#endregion

		#region Error logging
		public Exception GetLastException(Guid agentId)
		{
			//Exception ex;
			//if (lastErrors.TryGetValue(agentId, out ex))
			//    return ex;
			return null;
		}

		private string GetExceptionMessages(Exception ex)
		{
			string msg = ex.Message;
			if (ex.InnerException != null)
				msg += GetExceptionMessages(ex.InnerException);
			return msg;
		}

		public void Error(Guid agentId, string message, params object[] p)
		{
			Error(agentId, new Exception(string.Format(message, p)));
		}

		public void Error(Guid agentId, Exception ex)
		{
			//if (lastErrors.ContainsKey(agentId))
			//    lastErrors[agentId] = ex;
			//else
			//    lastErrors.Add(agentId, ex);
			Agent me;
			TryGetMyself(agentId, out me);
			ErrorLog err = new ErrorLog { Id = Guid.NewGuid().ToLong(), Message = GetExceptionMessages(ex), TimeStamp = DateTime.Now, AgentId = me.Id };
			if (ex.StackTrace != null)
				err.StackTrace = ex.StackTrace;
			Db.PersistEntity(err);
			Db.Commit();
		}

		public bool TryGetMyself(Guid agentId, out Agent me)
		{
			return TryGetMyself(agentId, out me, false, false);
		}

		/// <summary>
		/// Gets the agent in question
		/// </summary>
		/// <param name="agentId">id of the agent</param>
		/// <param name="me">the returning agent object</param>
		/// <param name="includeGateway">include Gateway reference</param>
		/// <param name="includeGatewayComputerSystem">include Gateway's computersystem reference (implies includeGateway=true)</param>
		/// <returns>true if successful, otherwise false</returns>
		public bool TryGetMyself(Guid agentId, out Agent me, bool includeGateway, bool includeGatewayComputerSystem)
		{
			AgentSessionObject aso =World.Instance.GetAgentSession(agentId);
			if (includeGatewayComputerSystem)
			{
				me = (from Agent a in Db where a.Id == aso.AgentInDb.Id select a).FirstOrDefault();
				Db.Activate(me, 3); //get agent -> gateway -> computersystem
				if (me.Gateway.ComputerSystem == null)
					Db.Activate(me.Gateway.ComputerSystem, 2);
			}
			else if (includeGateway)
			{
				me = (from Agent item in Db where item.Id == aso.AgentInDb.Id select item).FirstOrDefault();
				Db.Activate(me, 2);
			}
			else
			{
				me = (from item in World.Instance.AgentCache where item.Id == aso.AgentInDb.Id select item).FirstOrDefault();
			}


			if (me != null)
				return true;
			return false;
		}
		#endregion

		#region legacy code
		//private Guid TryGetSystemId(SystemFile file)
		//{
		//    try
		//    {
		//        var parent = (from SystemDirectory d in db from SystemFile f in d.Files where f.ID == file.ID select d).FirstOrDefault();
		//        if (parent == null)
		//            return Guid.Empty;

		//        return TryGetSystemId(parent);
		//    }
		//    finally
		//    {
		//        db.Close();
		//    }
		//}

		//private Guid TryGetSystemId(SystemDirectory dir)
		//{
		//    //if(dir.EntityState == EntityState.Detached)
		//    //    return Guid.Empty;

		//    //if (!dir.ComputerSystemReference.IsLoaded)
		//    //    dir.ComputerSystemReference.Load();

		//    //if(dir.ComputerSystem == null)
		//    //    return Guid.Empty;

		//    //return dir.ComputerSystem.ID;
		//    var result = (from ComputerSystem cs in db where cs.RootDirectory.ID == dir.ID select cs).FirstOrDefault();
		//    if (result == null)
		//        return Guid.Empty;
		//    return result.ID;
		//}

		//private bool IsMyGateway(Guid agentId, Guid systemId)
		//{
		//    //var result = (from Gateway item in db where item.Agent.ID == agentId && item.ComputerSystem.ID == systemId select item).FirstOrDefault();

		//    var result = (from Agent a in db where a.ID == agentId select a.Gateway.ComputerSystem).FirstOrDefault();
		//    if (result == null)
		//        return false;
		//    if (result.ID == systemId)
		//        return true;
		//    return false;
		//}

		//private bool TryGetAgentConnection(Guid agentId, Guid targetSystemId, out AgentConnection connection)
		//{
		//    AgentSessionObject agentSession = null;
		//    connection = null;
		//    if (!World.Instance.AgentSessions.TryGetValue(agentId, out agentSession))
		//        return false;
		//    connection = (from c in agentSession.Connections where c.TargetSystemId == targetSystemId select c).FirstOrDefault();
		//    return connection != null;
		//}
		#endregion

		protected ComputerSystem GetHostByAddress(string address)
		{
			Match match = SystemAddressParser.Match(address);
			if (!match.Success)
				return null;

			string hostname = match.Groups["host"].Value;
			string corpname = match.Groups["corp"].Value;
			string tld = match.Groups["tld"].Value;
			return (
				from ComputerSystem cs in Db
				where
					cs.Name.Replace(" ", "").ToLower() == hostname &&
					cs.Corporation.Name.Replace(" ", "").ToLower() == corpname &&
					cs.Corporation.TopLevelDomain.Name.ToLower() == tld
				select cs).FirstOrDefault();
		}

		#region email
		protected bool GetEmailAccountByAddress(string address, out EmailAccount target)
		{
			target = null;
			Match match = EmailAddressParser.Match(address);
			if (!match.Success)
				return false;

			string senderName = match.Groups["name"].Value;
			string hostname = match.Groups["host"].Value;
			string corpname = match.Groups["corp"].Value;
			string tld = match.Groups["tld"].Value;
			ulong systemId = (
				from ComputerSystem cs in Db 
				where 
					cs.Name.Replace(" ", "").ToLower() == hostname && 
					cs.Corporation.Name.Replace(" ", "").ToLower() == corpname && 
					cs.Corporation.TopLevelDomain.Name.ToLower() == tld select cs.Id).FirstOrDefault();
			if (systemId == 0)
				return false;
			target = (
				from Agent a in Db 
					where a.EmailAccounts != null && a.EmailAccounts.Count > 0 
				from EmailAccount acct in a.EmailAccounts where acct.SystemId == systemId && acct.Name.ToLower() == senderName.ToLower() select acct).FirstOrDefault();
			if (target == null)
				return false;
			return true;
		}

		protected bool StoreNewEmailInAccount(EmailAccount account, EmailHeader header, EmailBody body, string notificationTargetAddress)
		{
			//create copy of body
			EmailBody newbody = new EmailBody { AttachmentIds = body.AttachmentIds, Id = Guid.NewGuid().ToLong(), Message = body.Message };
			EmailHeader newheader = new EmailHeader
			{
				Id = Guid.NewGuid().ToLong(),
				CCs = header.CCs,
				Recipients = header.Recipients,
				CreationTimestamp = header.CreationTimestamp,
				EmailBodyId = newbody.Id,
				From = header.From,
				Folder = EmailFolder.Inbox,
				Status = EmailMessageStatus.Unread,
				Subject = header.Subject
			};
			Db.PersistEntity(newbody);
			account.Emails.Add(newheader);
			Db.PersistEntity(account);

			//notify target account holder
			ulong agentId = (from Agent a in Db where a.EmailAccounts != null from EmailAccount ea in a.EmailAccounts where ea.Id == account.Id select a.Id).FirstOrDefault();
			if(agentId != 0)
			{
				AgentSessionObject aso = (from x in World.Instance.AgentSessions where x.Value.AgentInDb.Id == agentId select x.Value).FirstOrDefault();
				if(aso != null) //user is online, notify right away
				{
					aso.NotifyNewEmail(newheader.From, notificationTargetAddress);
				}
				else
				{
					//store a QueuedMailNotification for this agent, which they receive when they log in next time
					QueuedMailNotification qmn = new QueuedMailNotification {AgentId = agentId, Sender = newheader.From, Target = notificationTargetAddress, Id = Guid.NewGuid().ToLong()};
					Db.Store(qmn);					
				}
			}
			return true;
		}
		#endregion

		#region banking
		protected BankAccount GetBankAccountByAccountNumber(long accountNumber, ulong systemId)
		{
			return (from BankAccount ba in Db where ba.AccountNumber == accountNumber && ba.SystemId == systemId select ba).FirstOrDefault();
		}

		protected bool CheckIsValidAccountNumber(long accountNumber, ulong systemId)
		{
			if (accountNumber == 0)
				return false;
			if ((from BankAccount ba in Db where ba.SystemId == systemId && ba.AccountNumber == accountNumber select ba).Count() != 0)
				return false;
			return true;
		}

		protected ulong GetBankAccountSystemId(string bankname)
		{
			Match match = SystemAddressParser.Match(bankname);
			if (!match.Success)
				return 0;

			string hostname = match.Groups["host"].Value;
			string corpname = match.Groups["corp"].Value;
			string tld = match.Groups["tld"].Value;

			ulong systemId = (
				from ComputerSystem cs in Db
				where
					cs.Name.Replace(" ", "").ToLower() == hostname &&
					cs.Corporation.Name.Replace(" ", "").ToLower() == corpname &&
					cs.Corporation.TopLevelDomain.Name.ToLower() == tld
				select cs.Id).FirstOrDefault();
			return systemId;
		}

		protected long GetAccountNumberById(ulong accountId)
		{
			return (from BankAccount ba in Db where ba.Id == accountId select ba.AccountNumber).FirstOrDefault();
		}

		protected string GetBankAccountStringById(ulong accountId)
		{
			BankAccount bankAccount = (from BankAccount ba in Db where ba.Id == accountId select ba).FirstOrDefault();
			if(bankAccount == null)
				return null;
			return string.Format("{0}@{1}", bankAccount.AccountNumber, GetAccountBankNameById(accountId));
		}

		protected string GetAccountBankNameById(ulong accountId)
		{
			ComputerSystem sys = (from BankAccount ba in Db where ba.Id == accountId from ComputerSystem cs in Db where cs.Id == ba.SystemId select cs).FirstOrDefault();
			if (sys == null)
				return null;
			return sys.GetHostname();
		}

		protected long GetVerifiedBankAccountAmount(ulong accountId)
		{
			try
			{
				BankAccount ba = (from BankAccount a in Db where a.Id == accountId select a).FirstOrDefault();
				if(ba == null || ba.IsSystem) //system accounts (shops) can potentially have more than long.maxValue in transactions, so we avoid an overflow here.
				{
					return 0;
				}
				var outgoing = (from BankTransaction tr in Db where tr.SendingAccountId == accountId select tr);
				var incoming = (from BankTransaction tr in Db where tr.ReceivingAccountId == accountId select tr);
				//var together = incoming.ToList();
				//together.AddRange(outgoing);
				//var result =  together.Sum(t => (t.SendingAccountId == accountId) ? (t.Amount * -1) : t.Amount);
				var resultIncoming = incoming.Sum(t => t.Amount);
				var resultOutgoing = outgoing.Sum(t => t.Amount*-1);
				var endResult = resultIncoming + resultOutgoing;
				return endResult;
			}
			catch(Exception)
			{
				Console.WriteLine("Warning: Exception while calculating account balance. Possible overflow detected. Account ID: " + accountId);
				return 0;
			}
		}
		#endregion
	}
}